﻿//  (c)2012 Henrik Nilsson
//  http://idmcrisis.com

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Net;
using System.Security;
using Microsoft.MetadirectoryServices;
using Renci.SshNet;
using Renci.SshNet.Common;
using SevenZip;

namespace IdmCrisis.Fim.Connectors
{
    internal class RemoteFile
    {
        // Field for holding file contents between operations.
        private byte[] buffer;

        /// <summary>
        /// The Uri used for getting remote file.
        /// </summary>
        internal Uri RemoteUri { get; private set; }

        /// <summary>
        /// Path to local file representation.
        /// </summary>
        internal string LocalPath { get; private set; }

        /// <summary>
        /// Credential used to get remote file.
        /// </summary>
        internal NetworkCredential NetworkCredential { get; private set; }
        
        /// <summary>
        /// Constructor.
        /// </summary>
        private RemoteFile() {}

        /// <summary>
        /// Create Method.
        /// </summary>
        internal static RemoteFile Create(Uri remoteUri, string localPath, NetworkCredential networkCredential)
        {
            return new RemoteFile
            {
                RemoteUri = new Uri(remoteUri.ToString().SubstituteDateTimeFormat()), 
                LocalPath = localPath, 
                NetworkCredential = networkCredential
            };
        }

        /// <summary>
        /// Reads the local file for reading.
        /// </summary>
        internal RemoteFile ReadLocal()
        {
            try
            {
                buffer = File.ReadAllBytes(LocalPath);
            }
            catch (Exception ex)
            {
                throw new ExtensibleExtensionException(ex.Message, ex);
            }
            
            return this;
        }

        /// <summary>
        /// Saves the local file.
        /// </summary>
        internal RemoteFile WriteLocal()
        {
            File.WriteAllBytes(LocalPath, buffer);
            return this;
        }

        /// <summary>
        /// Downloads a file from FTP.
        /// </summary>
        internal RemoteFile DownloadFromFtp(bool usePassive, bool enableSsl, bool useBinary)
        {
            // Create object to communicate with the FTP server and prepare it.
            var request = GetFtpWebRequest(usePassive, enableSsl, useBinary);
            request.Method = WebRequestMethods.Ftp.DownloadFile;
            
            // Lets download the file.
            try
            {
                var response = (FtpWebResponse)request.GetResponse();

                using (var memoryStream = new MemoryStream())
                using (var responseStream = response.GetResponseStream())
                {
                    // Copy response content to a MemoryStream
                    if (responseStream != null)
                        responseStream.CopyTo(memoryStream);

                    // Get the contents of the stream.
                    buffer = memoryStream.ToArray();
                }
            }
            catch (Exception ex)
            {
                if (ex is WebException)
                    // Try entering PASV.
                    if (ex.Message.Contains("504") && !usePassive)
                        return DownloadFromFtp(true, enableSsl, useBinary);
                    else if (ex.Message.Contains("530"))
                        throw new BadServerCredentialsException(ex.Message, ex);
                    else
                        throw new ServerDownException(ex.Message, ex);

                throw new ExtensibleExtensionException(ex.Message, ex);
            }
            
            return this;
        }

        /// <summary>
        /// Uploads a file to FTP.
        /// </summary>
        internal RemoteFile UploadToFtp(bool usePassive, bool enableSsl, bool useBinary)
        {
            // Create object to communicate with the server and prepare it.
            var request = GetFtpWebRequest(usePassive, enableSsl, useBinary);
            request.Method = WebRequestMethods.Ftp.UploadFile;

            // Lets upload the file.
            try
            {
                using (var requestStream = request.GetRequestStream())
                {
                    requestStream.Write(buffer, 0, buffer.Length);
                    requestStream.Close();
                }

                var response = (FtpWebResponse)request.GetResponse();
                response.Close();
            }
            catch (Exception ex)
            {
                if (ex is WebException)
                    // Try entering PASV.
                    if (ex.Message.Contains("504") && !usePassive)
                        return DownloadFromFtp(true, enableSsl, useBinary);
                    else if (ex.Message.Contains("530"))
                        throw new BadServerCredentialsException(ex.Message, ex);
                    else
                        throw new ServerDownException(ex.Message, ex);

                throw new ExtensibleExtensionException(ex.Message, ex);
            }
            
            return this;
        }

        /// <summary>
        /// Downloads a file from SFTP (SSH).
        /// </summary>
        internal RemoteFile DownloadFromSFtp(SshAuthenticationMethod sFtpAuthenticationMethod, byte[] privateKey, SecureString passPhrase)
        {
            // Create Connection info object.
            var connectionInfo = GetSshConnectionInfo(sFtpAuthenticationMethod, privateKey, passPhrase);
            
            // Create client object.
            try
            {
                using (var sftpClient = new SftpClient(connectionInfo))
                {
                    sftpClient.Connect();

                    using (var memoryStream = new MemoryStream())
                    {
                        // Download file and store file in buffer.
                        sftpClient.DownloadFile(RemoteUri.PathAndQuery, memoryStream);
                        buffer = memoryStream.ToArray();
                    }

                    sftpClient.Disconnect();
                }
            }
            catch (Exception ex)
            {
                if (ex is SshConnectionException || ex is SshOperationTimeoutException || ex is SftpPathNotFoundException)
                    throw new ServerDownException(ex.Message, ex);

                if (ex is SshAuthenticationException)
                    throw new BadServerCredentialsException(ex.Message, ex);

                throw new ExtensibleExtensionException(ex.Message, ex);
            }

            return this;
        }

        /// <summary>
        /// Uploads a file to SFTP (SSH).
        /// </summary>
        internal RemoteFile UploadToSFtp(SshAuthenticationMethod sFtpAuthenticationMethod, byte[] privateKey, SecureString passPhrase)
        {
            // Create Connection info object.
            var connectionInfo = GetSshConnectionInfo(sFtpAuthenticationMethod, privateKey, passPhrase);

            try
            {
                // Lets upload the file...
                using (var sftpClient = new SftpClient(connectionInfo))
                {
                    sftpClient.Connect();

                    using (var memoryStream = new MemoryStream(buffer))
                        sftpClient.UploadFile(memoryStream, RemoteUri.PathAndQuery);

                    sftpClient.Disconnect();
                }
            }
            catch (Exception ex)
            {
                if (ex is SshConnectionException || ex is SshOperationTimeoutException || ex is SftpPathNotFoundException)
                    throw new ServerDownException(ex.Message, ex);

                if (ex is SshAuthenticationException)
                    throw new BadServerCredentialsException(ex.Message, ex);

                throw new ExtensibleExtensionException(ex.Message, ex);
            }
            
            return this;
        }

        /// <summary>
        /// Downloads a file from SCP (SSH).
        /// </summary>
        internal RemoteFile DownloadFromScp(SshAuthenticationMethod sFtpAuthenticationMethod, byte[] privateKey, SecureString passPhrase)
        {
            // Create Connection info object.
            var connectionInfo = GetSshConnectionInfo(sFtpAuthenticationMethod, privateKey, passPhrase);
            
            // Lets download the file...
            try
            {
                using (var scpClient = new ScpClient(connectionInfo))
                {
                    scpClient.Connect();

                    using (var memoryStream = new MemoryStream())
                    {
                        // Download file and store file in buffer.
                        scpClient.Download(RemoteUri.PathAndQuery, memoryStream);
                        buffer = memoryStream.ToArray();
                    }

                    scpClient.Disconnect();
                }
            }
            catch (Exception ex)
            {
                if (ex is SshConnectionException || ex is SshOperationTimeoutException || ex is SftpPathNotFoundException)
                    throw new ServerDownException(ex.Message, ex);

                if (ex is SshAuthenticationException)
                    throw new BadServerCredentialsException(ex.Message, ex);

                throw new ExtensibleExtensionException(ex.Message, ex);
            }

            return this;
        }

        /// <summary>
        /// Uploads a file to SCP (SSH).
        /// </summary>
        internal RemoteFile UploadToScp(SshAuthenticationMethod sFtpAuthenticationMethod, byte[] privateKey, SecureString passPhrase)
        {
            // Create Connection info object.
            var connectionInfo = GetSshConnectionInfo(sFtpAuthenticationMethod, privateKey, passPhrase);

            // Lets upload the file...
            try
            {
                using (var scpClient = new ScpClient(connectionInfo))
                {
                    scpClient.Connect();

                    using (var memoryStream = new MemoryStream(buffer))
                        scpClient.Upload(memoryStream, RemoteUri.PathAndQuery);

                    scpClient.Disconnect();
                }
            }
            catch (Exception ex)
            {
                if (ex is SshConnectionException || ex is SshOperationTimeoutException || ex is SftpPathNotFoundException)
                    throw new ServerDownException(ex.Message, ex);

                if (ex is SshAuthenticationException)
                    throw new BadServerCredentialsException(ex.Message, ex);

                throw new ExtensibleExtensionException(ex.Message, ex);
            }

            return this;
        }

        /// <summary>
        /// Downloads a file from the file system, local or remote with optional impersonation.
        /// </summary>
        internal RemoteFile OpenFromFileSystem()
        {
            try
            {
                // Lets get the file.
                if (!string.IsNullOrEmpty(NetworkCredential.UserName) && !string.IsNullOrEmpty(NetworkCredential.Domain))
                {
                    using (new Impersonator(NetworkCredential))
                        buffer = File.ReadAllBytes(RemoteUri.LocalPath);
                }
                else
                    buffer = File.ReadAllBytes(RemoteUri.LocalPath);
            }
            catch (Exception ex)
            {
                if (ex is Win32Exception || ex is UnauthorizedAccessException || ex is SecurityException)
                    throw new BadServerCredentialsException(ex.Message, ex);

                if (ex is FileNotFoundException)
                    throw new ServerDownException(ex.Message, ex);

                throw new ExtensibleExtensionException(ex.Message, ex);
            }

            return this;
        }

        /// <summary>
        /// Uploads a file to the file system, local or remote with optional impersonation.
        /// </summary>
        internal RemoteFile SaveToFileSystem()
        {
            try
            {
                // Lets store the file.
                if (!string.IsNullOrEmpty(NetworkCredential.UserName) && !string.IsNullOrEmpty(NetworkCredential.Domain))
                {
                    using (new Impersonator(NetworkCredential))
                        File.WriteAllBytes(RemoteUri.LocalPath, buffer);
                }
                else
                    File.WriteAllBytes(RemoteUri.LocalPath, buffer);
            }
            catch (Exception ex)
            {
                if (ex is Win32Exception || ex is UnauthorizedAccessException || ex is SecurityException)
                    throw new BadServerCredentialsException(ex.Message, ex);

                if (ex is FileNotFoundException)
                    throw new ServerDownException(ex.Message, ex);

                throw new ExtensibleExtensionException(ex.Message, ex);
            }

            return this;
        }

        /// <summary>
        /// Compresses a file.
        /// </summary>
        internal RemoteFile Compress(bool useCompression, OutArchiveFormat outArchiveFormat, string archiveInternalFileName, bool useEncryption, SecureString password, ZipEncryptionMethod zipEncryptionMethod, string zipLibraryPath)
        {
            if (!useCompression)
                return this;

            SevenZipBase.SetLibraryPath(zipLibraryPath);

            var compressor = new SevenZipCompressor {ArchiveFormat = outArchiveFormat};
            if (useEncryption && outArchiveFormat == OutArchiveFormat.Zip)
                compressor.ZipEncryptionMethod = zipEncryptionMethod;

            try
            {
                using (var outMemoryStream = new MemoryStream())
                using (var inMemoryStream = new MemoryStream(buffer))
                {
                    if (useEncryption && password.Length > 0)
                        compressor.CompressStreamDictionary(
                            new Dictionary<string, Stream> {{ archiveInternalFileName.SubstituteDateTimeFormat(), inMemoryStream }}, 
                            outMemoryStream,
                            password.Decrypt());
                    else
                        compressor.CompressStreamDictionary(
                            new Dictionary<string, Stream> {{ archiveInternalFileName.SubstituteDateTimeFormat(), inMemoryStream }},
                            outMemoryStream);

                    buffer = outMemoryStream.ToArray();
                }
            }
            catch (Exception ex)
            {
                throw new ExtensibleExtensionException(ex.Message, ex);
            }
            
            return this;
        }

        /// <summary>
        /// Extracts a file
        /// </summary>
        internal RemoteFile Extract(bool useCompression, string archiveInternalFileName, bool useEncryption, SecureString password, string zipLibraryPath)
        {
            if (!useCompression)
                return this;

            try
            {
                using (var inMemoryStream = new MemoryStream(buffer))
                using (var outMemoryStream = new MemoryStream())
                {
                    SevenZipBase.SetLibraryPath(zipLibraryPath);

                    var extractor = password.Length > 0 && useEncryption ? 
                        new SevenZipExtractor(inMemoryStream, password.Decrypt()) : 
                        new SevenZipExtractor(inMemoryStream);

                    // Extract and write to buffer, use fileIndex if fileName is numeric.
                    int fileIndex;
                    if (int.TryParse(archiveInternalFileName, out fileIndex))
                        extractor.ExtractFile(fileIndex, outMemoryStream);
                    else
                        extractor.ExtractFile(archiveInternalFileName.SubstituteDateTimeFormat(), outMemoryStream);
                    
                    buffer = outMemoryStream.ToArray();
                }
            }
            catch (Exception ex)
            {
                throw new ExtensibleExtensionException(ex.Message, ex);
            }
            
            return this;
        }

        // Crates an array of SFTP authentication methods and populates it with the selected one.
        private ConnectionInfo GetSshConnectionInfo(SshAuthenticationMethod sshAuthenticationMethod, byte[] privateKey, SecureString passPhrase)
        {
            var authenticationMethods = new List<AuthenticationMethod>();
            switch (sshAuthenticationMethod)
            {
                case SshAuthenticationMethod.PrivateKey:
                {
                    using (var memoryStream = new MemoryStream(privateKey))
                    {
                        if (passPhrase != null)
                        {
                            var privateKeyFiles = new[] {new PrivateKeyFile(memoryStream, passPhrase.Decrypt())};
                            authenticationMethods.Add(new PrivateKeyAuthenticationMethod(NetworkCredential.UserName,
                                                                                         privateKeyFiles));
                        }
                        else
                        {
                            var privateKeyFiles = new[] {new PrivateKeyFile(memoryStream)};
                            authenticationMethods.Add(new PrivateKeyAuthenticationMethod(NetworkCredential.UserName,
                                                                                         privateKeyFiles));
                        }
                    }
                    break;
                }
                case SshAuthenticationMethod.Password:
                {
                    authenticationMethods.Add(new PasswordAuthenticationMethod(NetworkCredential.UserName, NetworkCredential.Password));
                    break;
                }
                case SshAuthenticationMethod.None:
                {
                    authenticationMethods.Add(new NoneAuthenticationMethod(NetworkCredential.UserName));
                    break;
                }
            }

            return new ConnectionInfo(RemoteUri.Host, RemoteUri.Port, NetworkCredential.UserName, authenticationMethods.ToArray());
        }

        // Creates an FTP request object.
        private FtpWebRequest GetFtpWebRequest(bool usePassive, bool enableSsl, bool useBinary)
        {
            // Create object to communicate with the server and prepare it.
            var request = (FtpWebRequest)WebRequest.Create(RemoteUri);
            request.Credentials = NetworkCredential;
            request.UsePassive = usePassive;
            request.EnableSsl = enableSsl;
            request.UseBinary = useBinary;
            
            return request;
        }
    }
}
